package com.gitee.wsl.coroutines.strategy.retry

import kotlin.math.max
import kotlin.math.min

/**
 * A [RetryPolicy] evaluates a [RetryFailure] in context of the current
 * [RetryStatus] and returns a [RetryInstruction].
 */
typealias RetryPolicy<E> = suspend RetryFailure<E>.() -> RetryInstruction

/**
 * Creates a combined [RetryPolicy] of [this] and the [other].
 *
 * If either policy returns an [instruction][RetryInstruction] to
 * [StopRetrying], the combined [policy][RetryPolicy] will return an
 * [instruction][RetryInstruction] to [StopRetrying].
 *
 * If both [policies][RetryPolicy] return an [instruction][RetryInstruction] to
 * [RetryAfter] a given delay, the combined [policy][RetryPolicy] will return
 * an [instruction][RetryInstruction] to [RetryAfter] the larger delay.
 */
operator fun <E> RetryPolicy<E>.plus(other: RetryPolicy<E>): RetryPolicy<E> = {
    val a = invoke(this)
    val b = other.invoke(this)

    when {
        a == StopRetrying || b == StopRetrying -> StopRetrying
        a == ContinueRetrying && b == ContinueRetrying -> ContinueRetrying
        else -> RetryAfter(max(a.delayMillis, b.delayMillis))
    }
}

/* https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ */

/**
 * Creates a [RetryPolicy] that returns an [instruction][RetryInstruction] to
 * [RetryAfter] an amount of milliseconds between [base] and [max] inclusive,
 * increasing the delay by 2 to the power of the number of attempts made.
 */
fun binaryExponentialBackoff(base: Long, max: Long): RetryPolicy<*> {
    require(base > 0) { "base must be positive: $base" }
    require(max > 0) { "max must be positive: $max" }

    return {
        /* sleep = min(cap, base * 2 ** attempt) */
        val delay = min(max, base saturatedMultiply attempt.binaryExponential())

        RetryAfter(delay)
    }
}

@Suppress("NOTHING_TO_INLINE")
inline fun binaryExponentialBackoff(range: LongRange): RetryPolicy<*> {
    return binaryExponentialBackoff(range.first, range.last)
}

/**
 * Creates a [RetryPolicy] that returns an [instruction][RetryInstruction] to
 * [RetryAfter] a random amount of milliseconds between 0 and [max] inclusive,
 * increasing the delay by 2 to the power of number of attempts made.
 */
fun fullJitterBackoff(base: Long, max: Long): RetryPolicy<*> {
    require(base > 0) { "base must be positive: $base" }
    require(max > 0) { "max must be positive: $max" }

    return {
        /* sleep = random_between(0, min(cap, base * 2 ** attempt)) */
        val delay = min(max, base saturatedMultiply attempt.binaryExponential())
        val randomDelay = random.nextLong(delay saturatedAdd 1)
        if (randomDelay == 0L) ContinueRetrying else RetryAfter(randomDelay)
    }
}

@Suppress("NOTHING_TO_INLINE")
inline fun fullJitterBackoff(range: LongRange): RetryPolicy<*> {
    return fullJitterBackoff(range.first, range.last)
}

/**
 * Creates a [RetryPolicy] that returns an [instruction][RetryInstruction] to
 * [RetryAfter] an amount of milliseconds equally portioned between the
 * [binaryExponentialBackoff] and [fullJitterBackoff].
 */
fun equalJitterBackoff(base: Long, max: Long): RetryPolicy<*> {
    require(base > 0) { "base must be positive: $base" }
    require(max > 0) { "max must be positive: $max" }

    return {
        /* temp = min(cap, base * 2 ** attempt) */
        val delay = min(max, base saturatedMultiply attempt.binaryExponential())

        /* sleep = temp / 2 + random_between(0, temp / 2) */
        val randomDelay = (delay / 2) saturatedAdd random.nextLong((delay / 2) saturatedAdd 1)

        RetryAfter(randomDelay)
    }
}

@Suppress("NOTHING_TO_INLINE")
inline fun equalJitterBackoff(range: LongRange): RetryPolicy<*> {
    return equalJitterBackoff(range.first, range.last)
}

/**
 * Creates a [RetryPolicy] that returns an [instruction][RetryInstruction] to
 * [RetryAfter] a random amount of milliseconds between [base] and [max]
 * inclusive, increasing by 3 times the previous delay.
 */
fun decorrelatedJitterBackoff(base: Long, max: Long): RetryPolicy<*> {
    require(base > 0) { "base must be positive: $base" }
    require(max > 0) { "max must be positive: $max" }

    return {
        /* sleep = min(cap, random_between(base, sleep * 3)) */
        val delay = max(base, previousDelay saturatedMultiply 3)
        val randomDelay = min(max, random.nextLong(base, delay saturatedAdd 1))

        RetryAfter(randomDelay)
    }
}

@Suppress("NOTHING_TO_INLINE")
inline fun decorrelatedJitterBackoff(range: LongRange): RetryPolicy<*> {
    return decorrelatedJitterBackoff(range.first, range.last)
}

/**
 * Creates a [RetryPolicy] that returns an [instruction][RetryInstruction] to
 * [RetryAfter] the specified [delayMillis].
 */
fun constantDelay(delayMillis: Long): RetryPolicy<*> {
    require(delayMillis > 0) { "delayMillis must be positive: $delayMillis" }

    return {
        RetryAfter(delayMillis)
    }
}

/**
 * Creates a [RetryPolicy] that returns an [instruction][RetryInstruction] to
 * [RetryAfter] the specified [delayMillis] if [this] policy returns an
 * [instruction][RetryInstruction] to [RetryAfter] an amount of time that is
 * greater than [delayMillis].
 */
fun <E> RetryPolicy<E>.maxDelay(delayMillis: Long): RetryPolicy<E> {
    require(delayMillis > 0) { "delayMillis must be positive: $delayMillis" }

    return {
        val instruction = this@maxDelay.invoke(this)

        if (instruction == StopRetrying || instruction == ContinueRetrying) {
            instruction
        } else {
            RetryAfter(min(instruction.delayMillis, delayMillis))
        }
    }
}

/**
 * Creates a [RetryPolicy] that returns an [instruction][RetryInstruction] to
 * [StopRetrying] after the [RetryStatus.attempt] reaches the specified
 * [limit].
 */
fun limitAttempts(limit: Int): RetryPolicy<*> {
    require(limit > 0) { "limit must be positive: $limit" }

    return {
        if (attempt + 1 >= limit) {
            StopRetrying
        } else {
            ContinueRetrying
        }
    }
}

/**
 * Creates a [RetryPolicy] that returns an [instruction][RetryInstruction] to
 * [StopRetrying] if [this] policy returns an [instruction][RetryInstruction]
 * to [RetryAfter] an amount of time that is greater than or equal to
 * [delayMillis].
 */
fun <E> RetryPolicy<E>.limitByDelay(delayMillis: Long): RetryPolicy<E> {
    require(delayMillis > 0) { "delayMillis must be positive: $delayMillis" }

    return {
        val instruction = this@limitByDelay.invoke(this)

        if (instruction == StopRetrying || instruction == ContinueRetrying) {
            instruction
        } else if (instruction.delayMillis >= delayMillis) {
            StopRetrying
        } else {
            instruction
        }
    }
}

/**
 * Creates a [RetryPolicy] that returns an [instruction][RetryInstruction] to
 * [StopRetrying] if [this] policy returns an [instruction][RetryInstruction]
 * to [RetryAfter] an amount of time that when added to the
 * [RetryStatus.cumulativeDelay] is greater than or equal to [delayMillis].
 */
fun <E> RetryPolicy<E>.limitByCumulativeDelay(delayMillis: Long): RetryPolicy<E> {
    require(delayMillis > 0) { "delayMillis must be positive: $delayMillis" }

    return {
        val instruction = this@limitByCumulativeDelay.invoke(this)

        if (instruction == StopRetrying || instruction == ContinueRetrying) {
            instruction
        } else {
            val delay = cumulativeDelay saturatedAdd instruction.delayMillis

            if (delay >= delayMillis) {
                StopRetrying
            } else {
                instruction
            }
        }
    }
}

/**
 * Creates a [RetryPolicy] that returns an [instruction][RetryInstruction] to
 * [ContinueRetrying] if the [RetryFailure] satisfies the given [predicate],
 * or an [instruction][RetryInstruction] to [StopRetrying] if it doesn't.
 */
inline fun <E> retryIf(crossinline predicate: RetryFailure<E>.() -> Boolean): RetryPolicy<E> = {
    if (predicate(this)) {
        ContinueRetrying
    } else {
        StopRetrying
    }
}

/**
 * Creates a [RetryPolicy] that returns an [instruction][RetryInstruction] to
 * [ContinueRetrying] if the [RetryFailure] _does not_ satisfy the given
 * [predicate], or an [instruction][RetryInstruction] to [StopRetrying] if it
 * does.
 */
inline fun <E> retryUnless(crossinline predicate: RetryFailure<E>.() -> Boolean): RetryPolicy<E> = {
    if (!predicate(this)) {
        ContinueRetrying
    } else {
        StopRetrying
    }
}